SPDX-FileCopyrightText: 2026 Jonathan Waerzeggers SPDX-FileCopyrightText: 2026 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
import math # On importe le module pour faire des calculs mathématiques (comme le sinus)
import random # On importe le module pour générer des nombres aléatoires
import bpy # On importe le module pour contrôler l'interface et les objets de Blender
import mathutils # On importe les outils pour manipuler les positions et vecteurs 3DVARIABLES DE CONFIGURATION ET ALLÉATOIRES
la_taille = 10 # Taille de base pour la création des deux blocs cubes
div = 5 # Nombre de coupures pour diviser les faces et pouvoir les déformer proprement
hasard = random.randint(
-2, 2
) # Nombre aléatoire entre -2 et 2 pour modifier la forme du sol (socle)
hasard_plafond = random.randint(
-2, 2
) # Nombre aléatoire entre -2 et 2 pour modifier la forme du toit (plafond)
if (
hasard_plafond == hasard
): # Si les deux nombres aléatoires sont identiques, on force le plafond à changer
hasard_plafond = (
-hasard if hasard != 0 else 2
) # Le plafond prend l'opposé du sol, ou prend la valeur 2 si le sol vaut zéro
liste_des_z = [] # Création d'une liste vide pour enregistrer des hauteurs précalculées
for i in range(
6
): # On crée une boucle qui va tourner 6 fois pour calculer des vagues de base
liste_des_z.append(
math.sin((2 * 3.14159 / 6) * i)
) # On calcule une valeur de sinus régulière et on l'ajoute dans notre listeFONCTIONS DE CALCUL DU RELIEF DES VAGUES
def le_calcul_du_z_socle(
x, y
): # Définition de la formule mathématique qui donne sa forme au sol (le socle)
mon_z = 0.0 # On commence avec une hauteur de déformation égale à zéro
if x < 1: # Si le point est situé à gauche (coordonnée X plus petite que 1)
mon_z = mon_z + 2.0 # On monte le niveau du sol de 2 unités de hauteur
if (
x > -3 and x < 3
): # Si le point se trouve dans la zone centrale (X compris entre -3 et 3)
if (
y > -3 and y < 3
): # Et si le point est aussi centré sur l'axe de la profondeur (Y entre -3 et 3)
index = int(
(x + 3) % 6
) # On calcule un index d'onde cyclique en fonction de la position X
mon_z = (
mon_z + (liste_des_z[index] * hasard)
) # On ajoute l'ondulation de notre table de sinus multipliée par le hasard du sol
if (
x > 2
): # Si le point est situé à l'extrême droite (coordonnée X plus grande que 2)
mon_z = mon_z - (
(x - 2) * 0.4
) # On crée une pente douce qui descend vers la droite
return mon_z # On renvoie la hauteur finale calculée pour ce point précis du soldef le_calcul_du_z_plafond(
x, y
): # Définition de la formule mathématique qui donne sa forme au toit (le plafond)
mon_z = 0.0 # On commence avec une hauteur de déformation égale à zéro
if x > 1: # Si le point est situé à droite (coordonnée X plus grande que 1)
mon_z += 1.8 # On ajoute une épaisseur vers le bas de 1.8 unités
if (
-3.5 < x < 3.5 and -3.5 < y < 3.5
): # Si le point est confiné au centre du modèle (X et Y entre -3.5 et 3.5)
idx_principale = int(
(x + 4) % 6
) # On calcule un index d'onde cyclique adapté pour le plafond
onde_principale = (
liste_des_z[idx_principale] * hasard_plafond * 1.3
) # On calcule une grosse ondulation sur l'axe X selon le hasard du plafond
onde_secondaire = (
math.sin(y * 1.5) * 0.7
) # On croise cette ondulation avec une deuxième vague sur l'axe Y
micro_bruit = (
math.cos(x * 2.5) * 0.3
) # On rajoute des micro-bosses très serrées pour faire un effet rugueux
mon_z += (
onde_principale + onde_secondaire + micro_bruit
) # On additionne toutes ces ondes ensemble pour sculpter l'intérieur
if x < -1: # Si le point est situé à gauche (coordonnée X plus petite que -1)
mon_z -= (
abs(x) - 1
) * 0.35 # On fait plonger la forme du plafond vers le bas sur le côté gauche
return mon_z # On renvoie la hauteur finale calculée pour ce point précis du toitFONCTIONS DE GÉNÉRATION DES MAILLAGES
def creation_socle(): # Fonction qui va fabriquer et déformer le bloc du bas
bpy.ops.mesh.primitive_cube_add(
size=la_taille, location=(0, 0, 0)
) # On ajoute un cube parfait au centre de la scène 3D
bloc = (
bpy.context.active_object
) # On récupère ce cube pour pouvoir le modifier dans le script
bloc.name = "Socle" # On lui donne le nom de "Socle" dans notre projet
bloc.scale[2] = (
0.4 # On l'écrase sur l'axe vertical Z pour en faire une dalle plate
)
bpy.ops.object.transform_apply(
location=False, rotation=False, scale=True
) # On applique l'échelle pour valider la nouvelle taille de l'objet
bpy.ops.object.mode_set(
mode="EDIT"
) # On passe en mode Édition pour avoir accès à la géométrie de l'objet
bpy.ops.mesh.subdivide(
number_cuts=div
) # On subdivise les faces du cube pour ajouter de nombreux petits carrés à déformer
bpy.ops.object.mode_set(
mode="OBJECT"
) # On revient en mode Objet pour appliquer les calculs sur les points
for le_point in (
bloc.data.vertices
): # On passe en revue un par un tous les sommets (points) du socle
xx = le_point.co.x # On lit sa position sur l'axe X
yy = le_point.co.y # On les sa position sur l'axe Y
deformation = le_calcul_du_z_socle(
xx, yy
) # On calcule la déformation grâce à notre formule mathématique du sol
if (
le_point.co.z < 0
): # Si le point est situé sous la ligne du milieu (la face du dessous du cube)
le_point.co.z = (
-la_taille / 2.0
) # On le force à rester parfaitement plat pour que le dessous reste droit
else: # Si le point est situé sur la face du dessus
le_point.co.z = (
-3.5 + deformation
) # On modifie sa hauteur Z en appliquant la déformation calculée
lissage = bloc.modifiers.new(
name="Lissage", type="SUBSURF"
) # On crée un modificateur pour lisser la surface du socle
lissage.levels = (
4 # On règle le niveau de lissage à 4 pour avoir une belle courbe bien propre
)
bpy.ops.object.shade_smooth() # On active le lissage visuel des ombres pour effacer l'aspect facettédef creation_plafond(): # Fonction qui va fabriquer et déformer le bloc du haut
bpy.ops.mesh.primitive_cube_add(
size=la_taille, location=(0, 0, 0)
) # On ajoute un cube parfait au centre de la scène 3D
bloc = bpy.context.active_object # On sélectionne ce nouveau cube
bloc.name = "Plafond" # On le nomme "Plafond" dans la liste des objets
bloc.scale[2] = (
0.4 # On l'écrase verticalement sur l'axe Z pour faire une dalle fine
)
bpy.ops.object.transform_apply(
location=False, rotation=False, scale=True
) # On valide cette modification d'échelle
bpy.ops.object.mode_set(mode="EDIT") # On passe en mode Édition
bpy.ops.mesh.subdivide(
number_cuts=div
) # On subdivise l'objet pour générer un maillage détaillé
bpy.ops.object.mode_set(mode="OBJECT") # On repasse en mode Objet
for le_point in bloc.data.vertices: # On analyse tous les sommets du bloc supérieur
xx = le_point.co.x # On lit la position X du sommet
yy = -le_point.co.y # On inverse la position Y pour créer un effet de symétrie inversée avec le bas
deformation = le_calcul_du_z_plafond(
xx, yy
) # On calcule la déformation du toit avec notre formule mathématique du plafond
if (
le_point.co.z > 0
): # Si le point est situé tout en haut (le toit extérieur du modèle)
le_point.co.z = la_taille / 2.0 # On le force à rester aligné et plat
else: # Si le point est situé en dessous (l'intérieur de notre grotte)
le_point.co.z = (
1.5 - deformation
) # On épaissit le bloc à 1.5 et on applique le relief vers
lissage = bloc.modifiers.new(
name="Lissage", type="SUBSURF"
) # On ajoute le modificateur de subdivision de surface pour lisser la voûte
lissage.levels = 4 # On pousse la qualité géométrique à 4
bpy.ops.object.shade_smooth() # On active l'ombrage lissé pour adoucir le rendu visuelFONCTIONS DE DISTRIBUTION DES PIQUES (LANCER DE RAYONS)
def creation_piques(): # Fonction qui va peupler l'espace vide avec des stalactites et stalagmites
bpy.context.view_layer.update() # On demande à Blender de rafraîchir l'emplacement exact de la géométrie des blocs
mon_graphe = bpy.context.evaluated_depsgraph_get() # On récupère la carte physique finale et réelle de nos objets (graphe de dépendance)
i = 0 # On initialise un compteur à zéro pour gérer notre boucle
while i < 120: # On lance une boucle pour faire 120 essais de création de piques
choix_zone = random.choice(
["bord_gauche", "inter_gauche", "inter_droite", "bord_droit"]
) # On tire au sort une zone pour éviter de mettre des piques là où il y aura les trous
if (
choix_zone == "bord_gauche"
): # Répartition des coordonnées X selon la zone tirée au sort
pos_x = random.uniform(-4.3, -3.3)
elif choix_zone == "inter_gauche":
pos_x = random.uniform(-1.7, -0.8)
elif choix_zone == "inter_droite":
pos_x = random.uniform(0.8, 1.7)
else:
pos_x = random.uniform(3.3, 4.3)
pos_y = random.uniform(
-3.5, 3.5
) # On choisit une coordonnée Y au hasard sur l'axe de la profondeur
origine = mathutils.Vector(
(pos_x, pos_y, 0.0)
) # On définit un point de départ virtuel en plein centre de la grotte (Z = 0.0)
v_bas = mathutils.Vector(
(0.0, 0.0, -1.0)
) # On prépare un vecteur directionnel vertical qui pointe vers le bas
v_haut = mathutils.Vector(
(0.0, 0.0, 1.0)
) # On prépare un vecteur directionnel vertical qui pointe vers le haut
touche_bas, loc1, n1, i1, obj1, m1 = bpy.context.scene.ray_cast(
mon_graphe, origine, v_bas
) # On jette un rayon laser vers le bas pour détecter l'impact sur le sol (loc1)
touche_haut, loc2, n2, i2, obj2, m2 = bpy.context.scene.ray_cast(
mon_graphe, origine, v_haut
) # On jette un rayon laser vers le haut pour détecter l'impact sur le plafond (loc2)
if (
touche_bas == True and touche_haut == True
): # Si nos deux lasers ont bien trouvé un obstacle solide en haut et en bas
hauteur_totale = (
loc2.z - loc1.z
) # On calcule l'espace de vide total disponible entre nos deux vagues
largeur = random.uniform(
0.3, 0.6
) # On choisit un diamètre aléatoire pour la base de notre future pique
chance = random.random() # On tire un nombre au sort entre 0.0 et 1.0 pour décider de la nature de la pique
if chance < 0.30: # Si le nombre est plus petit que 0.30 (30% de chances)
h_pilier = (
hauteur_totale + 0.5
) # CAS 1 : On crée un grand pilier porteur qui relie le bas et le haut
bpy.ops.mesh.primitive_cone_add(
vertices=3, radius1=largeur, depth=h_pilier, location=loc1
) # On génère un cône à 3 côtés posé à l'emplacement du sol (loc1)
le_pilier = bpy.context.active_object # On attrape ce nouveau cône
le_pilier.location.z = (
loc1.z + (h_pilier / 2.0) - 0.5
) # On décale sa hauteur pour que son socle plat s'encastre parfaitement dans le sol
le_pilier.rotation_euler = (
0,
0,
random.uniform(0, 6.28),
) # On lui applique une rotation aléatoire sur l'axe Z pour qu'il soit orienté de biais
elif chance > 0.80: # Si le nombre est plus grand que 0.80 (20% de chances)
h_stalactite = (
hauteur_totale * random.uniform(0.15, 0.40)
) + 0.5 # CAS 2 : On crée une stalactite suspendue au plafond
bpy.ops.mesh.primitive_cone_add(
vertices=3, radius1=largeur, depth=h_stalactite, location=loc2
) # On ajoute un cône à 3 côtés à l'emplacement du plafond (loc2)
la_stalactite = bpy.context.active_object # On attrape cette stalactite
la_stalactite.location.z = (
loc2.z - (h_stalactite / 2.0) + 0.5
) # On l'ajuste en hauteur pour qu'elle s'enracine proprement dans le bloc du haut
la_stalactite.rotation_euler = (
math.pi,
0,
random.uniform(0, 6.28),
) # On la retourne de 180 degrés (math.pi) sur l'axe X pour qu'elle pointe vers le bas
else: # Dans tous les autres cas (50% de chances restantes)
h_spike = (
(hauteur_totale * random.uniform(0.1, 0.45)) + 0.5
) # CAS 3 : On crée une stalagmite classique qui monte depuis le sol
bpy.ops.mesh.primitive_cone_add(
vertices=3, radius1=largeur, depth=h_spike, location=loc1
) # On crée le cône sur le sol (loc1)
le_spike = bpy.context.active_object # On attrape la stalagmite
le_spike.location.z = (
loc1.z + (h_spike / 2.0) - 0.5
) # On cale sa base plate sur le maillage de notre socle déformé
le_spike.rotation_euler = (
0,
0,
random.uniform(0, 6.28),
) # On lui donne un angle de rotation aléatoire sur l'axe vertical
i = i + 1 # On ajoute 1 au compteur pour valider notre itération et continuerFONCTIONS DE DÉCOUPE DES TROUS (OPÉRATIONS BOOLÉENNES)
def faire_les_trous_du_plafond(): # Fonction qui va venir creuser le plafond avec des gabarits cubes
le_plafond = bpy.data.objects.get(
"Plafond"
) # On récupère notre objet principal nommé "Plafond"
if (
le_plafond == None
): # Si par malheur le plafond n'est pas trouvé dans la scène, on annule tout
return # On quitte la fonction pour éviter de générer un message d'erreur
mes_trous = [] # On prépare une liste vide pour mettre nos blocs emporte-pièces
bpy.ops.mesh.primitive_cube_add(
size=1.0, location=(-2.5, 0.0, 5.0)
) # Fabrication du gabarit de découpe pour le côté gauche (X = -2.5)
t1 = bpy.context.active_object # On attrape ce premier cube outil
t1.scale = (
1.2,
3.5,
15.0,
) # On l'étire pour en faire un grand pavé rectangulaire capable de traverser le plafond
bpy.ops.object.transform_apply(
scale=True
) # On valide ses dimensions dans l'espace 3D
mes_trous.append(t1) # On l'ajoute à notre liste d'outils
bpy.ops.mesh.primitive_cube_add(
size=1.0, location=(0.0, 0.0, 6.0)
) # Fabrication du gabarit de découpe pour le centre (X = 0.0)
t2 = bpy.context.active_object # On attrape ce deuxième cube outil
t2.scale = (
1.2,
3.5,
20.0,
) # On lui donne des proportions allongées (encore plus haut sur Z pour assurer le coup)
bpy.ops.object.transform_apply(scale=True) # On applique les transformations
mes_trous.append(t2) # On l'ajoute à la liste
bpy.ops.mesh.primitive_cube_add(
size=1.0, location=(2.5, 0.0, 5.0)
) # Fabrication du gabarit de découpe pour le côté droit (X = 2.5)
t3 = bpy.context.active_object # On sélectionne le troisième cube outil
t3.scale = (
1.2,
3.5,
15.0,
) # On l'ajuste aux mêmes échelles que le premier rectangle de gauche
bpy.ops.object.transform_apply(scale=True) # On applique les transformations
mes_trous.append(t3) # On range ce dernier outil dans la liste
bpy.ops.object.select_all(
action="DESELECT"
) # On désélectionne tous les objets de l'interface Blender
for (
truc
) in mes_trous: # On fait une boucle sur nos trois objets gabarits rectangulaires
truc.select_set(True) # On les sélectionne tous ensemble
bpy.context.view_layer.objects.active = mes_trous[
0
] # On choisit le premier rectangle de la liste comme l'élément de référence actif
bpy.ops.object.join() # On fusionne les 3 rectangles en un seul et unique gros maillage outil (Join)
le_gros_trou = (
bpy.context.active_object
) # On associe notre variable à cet objet combiné final
le_booleen = le_plafond.modifiers.new(
name="Coupe", type="BOOLEAN"
) # On ajoute un modificateur de géométrie booléenne sur notre objet Plafond
le_booleen.operation = "DIFFERENCE" # On règle le modificateur sur le mode DIFFERENCE (Soustraction de matière)
le_booleen.object = le_gros_trou # On lui donne la cible : c'est notre gros objet outil à trois blocs qui va creuser
le_booleen.solver = "EXACT" # On sélectionne le solveur EXACT pour avoir des bords parfaits sur les trous
bpy.context.view_layer.objects.active = le_plafond # On remet le Plafond comme l'objet actif pour pouvoir valider l'opération
bpy.ops.object.modifier_apply(
modifier="Coupe"
) # On valide et on applique définitivement le trou dans la géométrie du plafond
bpy.data.objects.remove(
le_gros_trou, do_unlink=True
) # On supprime définitivement l'objet outil de la scène pour ne pas encombrer BlenderNETTOYAGE GÉNÉRAL DE LA SCÈNE
def nettoie_tout(): # Fonction qui nettoie les anciens essais avant de lancer une nouvelle génération
bpy.ops.object.select_all(
action="SELECT"
) # On ordonne la sélection de la totalité des objets de la scène
try: # On ouvre une sécurité pour éviter les plantages si la caméra n'existe pas
bpy.data.objects["Oblique Camera"].select_set(
False
) # On retire la caméra fixe nommée "Oblique Camera" de notre sélection pour ne pas la perdre
except: # Si Blender ne trouve pas cette caméra, on ignore simplement le problème
pass # On passe à l'étape suivante sans bloquer le script
bpy.ops.object.delete() # On efface tous les objets sélectionnés (les anciens socles, plafonds et cônes)
bpy.ops.outliner.orphans_purge() # On efface les blocs de données inutilisés dans la mémoire cache de BlenderCORPS PRINCIPAL DU PROGRAMME (LANCEMENT)
nettoie_tout() # Étape 1 : On efface les anciens objets de la scène 3D
creation_socle() # Étape 2 : On génère le socle du bas et on déforme sa surface
creation_plafond() # Étape 3 : On génère le plafond épais et volumineux
creation_piques() # Étape 4 : On peuple l'espace de la caverne en distribuant nos pointes cônes
faire_les_trous_du_plafond() # Étape 5 : En dernier, on applique la soustraction booléenne pour percer les fentes du toit